Conversation
…e flash
On Windows, Claude Code spawns each .claude/settings.json hook command via
its parent shell. The node.exe used by the bare `node "..."` form is a
console-subsystem binary, so even with CREATE_NO_WINDOW set by Claude Code's
spawn, a brief black console window flashes onscreen for every PostToolUse,
SessionStart, etc. fire — repeatedly throughout a session.
Wrap the command in `wscript //nologo "<vbs>" node "..."`. `wscript.exe` is
a windows-subsystem host, so the OS never allocates a console for it;
WScript.Shell.Run(cmd, 0, True) then invokes the underlying `node "..."`
with SW_HIDE and waits for completion, propagating the exit code so Claude
Code's hook contract is preserved.
POSIX hosts get the unchanged bare form — buildHookCommand falls back to
`node "$CLAUDE_PROJECT_DIR/.wolf/hooks/<x>.js"` when platform !== 'win32'
or the VBS asset isn't found.
Changes:
- assets/hook-runner.vbs — universal SW_HIDE wrapper (re-quotes args)
- scripts/copy-hook-runner.mjs — build step copies VBS to dist/assets/
- src/utils/hook-command.ts — buildHookCommand() + findHookRunnerVbs()
- src/cli/init.ts, src/cli/update.ts — call buildHookCommand() per hook
- package.json — build appends `node scripts/copy-hook-runner.mjs`
Existing projects need an `openwolf init` (or `openwolf update`) re-run for
the new command form to land in their .claude/settings.json; the
replaceOpenWolfHooks() matcher (".wolf/hooks/" substring) handles the
upgrade in place without touching user-authored hooks.
Companion to the Caliber wscript+VBS wrapper landed in
caliber-ai-org/ai-setup#fix/windows-hide-spawn — same approach, same VBS
shape, validated empirically on pandorum where zero flashes remained after
both wrappers were active.
|
Thanks @mann1x and @Mizuho0329. Your Windows wrapper proposal is credited; it has not been included, and console-flash validation remains open. |
|
Closing this — and the validation you asked for is the reason. It says the wrapper in this PR cannot ship, and it also says the goal is achievable by other means. Both halves are measured, on two independent hosts. The wrapper deadlocks every hook
Measured identically on GitHub
So it does not merely lose output: it hangs until Claude Code's per-hook timeout, on every tool call. That is worse than the flash it removes, and it is why this should not be merged. To be fair to the approach, the hiding part works. The same VBS, changed only from What actually causes the flashNot running node — console allocation. A parent process with no console of its own forces a console-subsystem child to allocate one, and that console is visible. Where the parent already owns a console, the child attaches to it and nothing appears. Measured on the Win10 desktop, interactive session:
That last row is worth pausing on before anyone spends effort here: in the shape Claude Code actually uses, from a terminal, there is no flash to fix. It appears when whatever spawns the hook has no console of its own — a GUI-launched parent, an extension host, a service. So this affects some users' setups and not others, which probably explains why it has been hard to pin down. A launcher that does workThe requirement is not "hide a window". It is: allocate no visible console, inherit the parent's std handles rather than replace them, and propagate the exit code. That means Both halves are load-bearing. My first attempt used .NET's With both flags, on the same desktop session as the table above:
Flash gone, hook contract intact. The honest cost, and the optionsThis needs a native binary — roughly 4 KB — which is a real change in what an npm package contains, and you may reasonably decline on that alone. The options, with what each actually costs:
My honest read is that (4) is the right first move and (2)+(3) is the only version of a local fix I would argue for. I am happy to open a separate PR for (2)+(3) if you want it, and equally happy for the answer to be no — the measurement is the part worth having either way. Both scripts are self-contained and on 🤖 Generated with Claude Code |
|
Closing per the measurement above: the wrapper as written hangs every hook, so there is nothing here to merge. The launcher alternative is a separate proposal if you want it — no obligation, and the upstream option costs you nothing. |
Summary
On Windows, every
.claude/settings.jsonhook that OpenWolf installs(
SessionStart,PreToolUse× 2,PostToolUse× 2,Stop— sixper project) currently uses the bare form:
When Claude Code fires the hook, it spawns this without
windowsHide: true.node.exeis a console-subsystem binary, soWindows allocates a fresh console window for every hook fire —
visible as a brief black flash. Under active editor use this stacks
to multiple flashes per second (every Edit/Write triggers
PreToolUse + PostToolUse). It steals focus from the editor and is
unusable in practice.
Anthropic acknowledged the upstream gap
(anthropics/claude-code#19012)
and closed it as not planned. The standard workaround — adopted
this PR — is a tiny VBS wrapper invoked via
wscript.exe.wscript.exeis a windows-subsystem host so the OS never allocatesa console for it;
WScript.Shell.Run(cmd, 0, True)then launchesthe underlying
node "..."withSW_HIDEand waits for completion,propagating the exit code so the Claude Code hook contract is
preserved.
The fix
A single new helper
buildHookCommand(scriptName)(insrc/utils/hook-command.ts) decides per-platform whether to wrap:Both
init.tsandupdate.tsroute their six hook-command stringsthrough this helper, so a
openwolf initoropenwolf updateafterthis lands rewrites existing projects'
.claude/settings.jsontothe wscript-wrapped form (replaceOpenWolfHooks() already filters by
.wolf/hooks/substring so the swap is in-place and idempotent).Cross-platform safety
buildHookCommandreturns the existing
node "..."shape whenprocess.platform !== 'win32'.same fallback — the helper returns the bare
node "..."shapeif
findHookRunnerVbs()can't locate the asset.openwolf init/openwolf updateswap; until then they keepflashing as today (no regression).
Files touched
assets/hook-runner.vbs0— SW_HIDE: never show a windowTrue— wait for completion, propagate exit codeC:\Program Files\nodejs\node.exe)survive the round-trip through
WScript.Arguments→ cmd stringscripts/copy-hook-runner.mjsTiny ESM build step that copies
assets/hook-runner.vbs→dist/assets/hook-runner.vbsnext to the compiled CLI. Already inthe
dist/glob shipped viapackage.jsonfilessonpm packpicks it up without further configuration.
src/utils/hook-command.tsTwo exports:
findHookRunnerVbs(): string | null— resolves the VBS path atruntime relative to the compiled module location (
dist/src/utils/→
dist/assets/)buildHookCommand(scriptName, platform?, vbsPath?)— emits theper-platform command string; both
platformandvbsPathareinjectable for tests
Verification
tscclean;node scripts/copy-hook-runner.mjscopiesthe VBS into
dist/assets/;npm packships it inside thetarball under the
dist/globbuildHookCommand('post-write.js', 'linux')returns the unchanged
node "$CLAUDE_PROJECT_DIR/.wolf/hooks/post-write.js"buildHookCommand('post-write.js')on the deployed install returns
wscript //nologo "C:/Users/manni/AppData/Roaming/npm/node_modules/openwolf/dist/assets/hook-runner.vbs" node "$CLAUDE_PROJECT_DIR/.wolf/hooks/post-write.js"openwolf initon three real Windows projects;.claude/settings.jsonnow contains the wscript-wrapped form forall six hooks per project; zero console flashes during an
extended Claude Code session, operator-confirmed
wrapper shape for Caliber's hook commands; the combined deploy on
pandorum eliminates every Windows-flash source observed during
the audit
Compatibility
replaceOpenWolfHooks()matches on.wolf/hooks/substring soit identifies both the old bare-
nodeand new wscript-wrappedshapes as OpenWolf-owned and rewrites in place — no stale
duplicates after upgrade
.wolf/hooks/) are preserved untouched, same as todayreplaceOpenWolfHookspath — they continue to pass because thehelper is platform-gated and they run on Linux
Status
Ready for review. The change is empirically validated end-to-end on
Windows 10 22H2 (pandorum, three real OpenWolf-managed projects,
extended Claude Code session — zero flashes from
.wolf/hooks).The companion Caliber PR
(caliber-ai-org/ai-setup#222)
ships the same VBS-wrapper shape for Caliber's hook commands and
has CI green across the 6-cell Linux/Windows × Node 20/22/25
matrix; combined deploy was the configuration that produced the
zero-flash result.